Skip to content

feat: add jsonToHtmlAsync and generic toTree<T>() with React primitives#100

Draft
glassdimlygr wants to merge 4 commits intocontentstack:masterfrom
glassdimlygr:feat/async-json-to-html
Draft

feat: add jsonToHtmlAsync and generic toTree<T>() with React primitives#100
glassdimlygr wants to merge 4 commits intocontentstack:masterfrom
glassdimlygr:feat/async-json-to-html

Conversation

@glassdimlygr
Copy link
Copy Markdown

@glassdimlygr glassdimlygr commented May 7, 2026

Summary

Adds two new output modes to the serializer, both purely additive — existing jsonToHtml / htmlToJson / jsonToMarkdown are untouched.

  1. jsonToHtmlAsync — async variant of jsonToHtml where handlers can return Promise<string>
  2. toTree<T>() — generic, framework-agnostic tree walker that outputs any type T
  3. /react entry point — reference implementation of toTree<ReactNode>() with default handlers (see discussion below)

jsonToHtmlAsync

Async version of toRedactor. Children are resolved concurrently via Promise.all. Handler results are awaited, so both sync and async return values work. No changes to existing jsonToHtml.


toTree<T>() — generic tree walker

A framework-agnostic tree walker in src/toTree.ts (~120 lines, zero dependencies). Accepts IJsonToTreeOptions<T>:

interface IJsonToTreeOptions<T> {
  elementTypes: Record<string, (jsonBlock: any, children: T | null) => T>
  textMarks?: Record<string, (children: T, value?: any) => T>
  createText: (text: string) => T
  createLineBreak: (key: string) => T
  combineChildren: (children: T[]) => T
  wrapTextAttrs?: (node: T, attrs: { classname?: string; id?: string }) => T
  wrapTextStyle?: (node: T, style: { color?: string; fontFamily?: string; fontSize?: string }) => T
  keyElement?: (element: T, key: string) => T
}

The walker handles recursion, text processing, and mark application. The caller provides all construction logic for their chosen output format.


/react entry point — discussion

This PR includes src/react.tsx, shipped as a separate entry point at @contentstack/json-rte-serializer/react. It provides:

  • reactPrimitivescreateText, createLineBreak, combineChildren, wrapTextAttrs, wrapTextStyle, keyElement
  • defaultElementTypes — handlers for all standard element types (p, h1h6, a, img, tables, grid, etc.)
  • defaultTextMarksbold<strong>, italic<em>, etc.
  • jsonToReact() — convenience wrapper that combines everything

React is an optional peer dependency and is externalized from the bundle. Non-React consumers never touch this entry point.

Question for maintainers

Should /react live in this repo, or should it be maintained externally?

Arguments for including it:

  • Without it, every React consumer has to implement the same ~30 default element handlers + 6 primitive callbacks — real boilerplate that's identical across projects
  • It's a natural complement to toTree<T>() — the generic API is only useful if there are reference implementations
  • Separate exports entry point means it's fully tree-shakeable and non-React consumers never see it
  • React is already in devDependencies (for the .tsx build)

Arguments for keeping it external:

  • Adds a framework opinion to a framework-agnostic library
  • Maintainers would need to update it when new element types are added
  • Could set a precedent (Vue adapter? Svelte adapter?)

Happy to split this PR and keep only jsonToHtmlAsync + toTree<T>() if you'd prefer /react to live elsewhere. The included implementation serves as documentation either way — it shows exactly how to wire up toTree for a virtual DOM framework.


What's NOT changed

  • jsonToHtml / toRedactor — untouched
  • htmlToJson / fromRedactor — untouched
  • jsonToMarkdown — untouched
  • All 127 existing tests pass
  • No breaking changes

Package structure

@contentstack/json-rte-serializer
├── index.js    — jsonToHtml, jsonToHtmlAsync, htmlToJson, jsonToMarkdown, toTree
└── react.js    — jsonToReact, reactPrimitives, defaultElementTypes, defaultTextMarks

@glassdimlygr glassdimlygr force-pushed the feat/async-json-to-html branch from 6cbbd2d to c2200a4 Compare May 7, 2026 15:14
Add toRedactorAsync (exported as jsonToHtmlAsync) that supports
customElementTypes handlers returning string | Promise<string>.
Enables dynamic component resolution (e.g. await import()) before
serialization. Children are resolved via Promise.all concurrently.

Refactors shared logic (text processing, attr building, element
node processing) into toRedactorHelpers.ts so both sync and async
versions are thin recursive shells with no duplicated code.

The existing sync jsonToHtml behavior is unchanged.

New types: IJsonToHtmlAsyncElementTags, IJsonToHtmlAsyncOptions.
@glassdimlygr glassdimlygr force-pushed the feat/async-json-to-html branch from c2200a4 to 44da5fa Compare May 7, 2026 15:56
Add toReactTree.tsx which walks the JSON RTE document and returns
ReactNode instead of HTML strings. Handlers receive (jsonBlock, children)
and return ReactNode directly, enabling real React component rendering
without renderToStaticMarkup.

- New exports: jsonToReact, IJsonToReactOptions, IJsonToReactElementHandler, IJsonToReactTextHandler
- React is a peer dependency (optional) and externalized from the bundle
- Default handlers map all standard element types to JSX equivalents
- Text mark handlers (bold, italic, etc.) wrap children in semantic elements
@glassdimlygr glassdimlygr changed the title feat: add jsonToHtmlAsync for async customElementTypes support feat: add jsonToHtmlAsync and jsonToReact output modes May 8, 2026
Replace React-specific toReactTree.tsx with two modules:

- src/toTree.ts: Generic tree walker that accepts IJsonToTreeOptions<T>
  with caller-provided callbacks (createText, createLineBreak,
  combineChildren, etc.). No React dependency, no JSX, no defaults.

- src/react.tsx: Separate entry point (@contentstack/json-rte-serializer/react)
  that provides ready-to-use React primitives, default element handlers,
  default text marks, and a jsonToReact() convenience wrapper.

Consumers import from /react to get the batteries-included experience,
or use toTree<T>() directly for Preact, Solid, Vue h(), etc.

React is externalized only from the /react entry point. The core
package has zero React dependency.
@glassdimlygr glassdimlygr changed the title feat: add jsonToHtmlAsync and jsonToReact output modes feat: add jsonToHtmlAsync and generic toTree<T>() with React primitives May 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant